vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 14


Ein paar längere Beispiele

Widmen wir den letzten Tag der Woche ein paar weiteren Beispielen. Sie werden in diesem Kapitel nicht viel Neues lernen, auch Übungen und Quiz gibt es heute nicht. Betrachten Sie es als kurze Verschnaufpause, in der Sie sich vollständige Perl-Skripts ganz in Ruhe ansehen können. Insgesamt hat das Buch drei dieser Beispiellektionen (immer am Ende der Wochen), die Ihr neues Wissen festigen.

Heute analysieren wir zwei längere Perl-Skripts:

Ein Adreßbuch zum Durchsuchen (adressen.pl)

Unser erstes Skript heute besteht aus zwei Teilen:

Dieses Skript nutzt so ziemlich alles, was Sie diese Woche gelernt haben: Skalar- und Hash-Daten, Bedingungen, Schleifen, Ein- und Ausgabe, Subroutinen, lokale Variablen und Mustervergleich. Es gibt sogar ab und zu einen Funktionsaufruf, um die Sache interessanter zu machen. Aber jetzt lassen Sie uns ohne weitere Umschweife beginnen.

Wie es funktioniert

Das Skript adressen.pl wird mit einem einzigen Argument aufgerufen: der Adreßdatei namens adressen.txt. Geben Sie den Aufruf in der Befehlszeile ein, wie Sie es bereits bei anderen Perl-Skripten gemacht haben:

% adressen.pl adressen.txt

Wenn Sie MacPerl verwenden, speichern Sie das Skript adressen.pl als Droplet und ziehen (Drag) Sie dann die Datei adressen.txt auf das Symbol adressen.pl, wo Sie diese ablegen (Drop).

Als erstes fragt das Adreßbuch-Skript Sie nach dem gesuchten Begriff:

Wonach soll gesucht werden? Johnson

Das Suchmuster, das Sie an adressen.pl übergeben, kann verschiedene Formen haben:

So liefert zum Beispiel die Suche nach dem Namen Johnson in meiner Beispieldatei adressen.txt folgende Ausgabe zurück:

*********************
Paul Johnson
212 345 9492
234 33rd St Apt 12C, NY, NY 10023
http://www.foo.org/users/don/paul.html
*********************
Alice Johnson
(502) 348 2387
(502) 348 2341
*********************
Mary Johnson
(408) 342 0999
(408) 323 2342
mj@asd.net
http://www.mjproductions.com
*********************

Alle Namen, Adressen, Telefonnummern und Webseiten dieser Beispiel-Adreßdatei sind natürlich frei erfunden. Irgendwelche Übereinstimmungen dieser Daten mit lebenden oder toten Personen sind reiner Zufall.

Die Adreßdatei

Das Herzstück eines Adreßbuches (das Sie selbst erstellen müssen, wenn Sie dieses Skript nutzen wollen) ist eine Datei von Adressen in einem speziellen Format, das von Perl verstanden wird. Sie können es auch als einfache textbasierte Datenbank betrachten und dann Perl-Skripts schreiben, die diese Datenbank um Datensätze (Adressen) ergänzen oder auch Datensätze löschen.

Das Format der Adreßbuch-Datei in seiner generischen Form sieht folgendermaßen aus:

Name: Name
Telefon: Nummer
Fax: Nummer
Adresse: Adresse
EMail: email-Adresse
URL: Web URL
---

zum Beispiel:

Name: Paul Johnson
Telefon: 212 345 9492
Adresse: 234 33rd St Apt 12C, NY, NY 10023
URL: http://www.foo.org/users/don/paul.html
---

Jeder Datensatz besteht aus einer Reihe von Feldern (Name, Telefon, Fax, Adresse, E-Mail und URL, die jedoch nicht alle erforderlich sind) und endet mit drei Gedankenstrichen. Die Feldnamen (Name, Telefon, URL usw.) sind von ihren Werten durch einen Doppelpunkt und ein Leerzeichen getrennt. Die Werte müssen kein spezielles Format aufweisen. Sie können weitere Feldnamen in die Datenbank aufnehmen, und die Suchschlüssel werden diese zusätzlichen Felder auch durchsuchen, aber in der Ausgabe werden diese Felder übergangen. (Wenn Sie wirklich ein zusätzliches Feld benötigen, zum Beispiel für eine Handy-Nummer, können Sie das Skript jederzeit ändern. Perl macht es Ihnen in dieser Hinsicht leicht.)

Sie können beliebig viele Adressen in die Datei adressen.txt aufnehmen. Doch je größer das Adreßbuch, um so länger die Zeitdauer, bis übereinstimmende Datensätze gefunden werden, da für jede Adresse die Datei von vorn bis hinten durchgegangen wird. Solange Sie jedoch keine vier bis fünf Millionen Freunde haben, werden Sie von Perls Anstrengungen nichts mitbekommen.

Das Skript

Das Skript adressen.pl liest die Datei adressen.txt Adresse für Adresse ein und gleicht dann das Suchmuster mit jeder Adresse ab. Der oberste Teil des Skripts ist eine while-Schleife, die diese Aufgabe übernimmt, wobei wiederum fünf weitere Subroutinen abgearbeitet werden, um die komplexeren Teile des Skripts zu bewältigen.

Lassen Sie uns ganz oben im Skript beginnen. Auf oberster Ebene definieren wir drei globale Variablen:

Der erste Schritt in dem äußeren Teil des Skripts besteht darin, zur Eingabe des Suchbegriffs aufzufordern und diesen dann in $search abzulegen:

$search = &getpattern();        # fragt nach dem Suchmuster

Die Subroutine &getpattern() besteht aus den grundlegenden Schritten »Eingabe lesen / zurechtschneiden / Ergebnis zurückliefern«, die Ihnen schon viel zu oft in diesem Buch begegnet sind:

sub getpattern {
my $in = ''; # Eingabe
print 'Wonach soll gesucht werden? ';
chomp($in = <STDIN>);
return $in;
}

Schritt zwei im äußeren Teil des Skripts ist eine endlose while-Schleife, die einen Datensatz einliest, das Suchmuster verarbeitet und bei Übereinstimmung den Datensatz ausgibt:

while () {              # durchsucht die Adressdatei
%rec = &read_addr();
if (%rec) { # Datensatz gefunden
&perform_search($search, %rec);
} else { # Ende der Adressdatei, Aufräumarbeiten
if (!$bigmatch) {
print "Nichts gefunden.\n";
} else { print "*********************\n"; }
last; # verlassen, wir sind fertig
}
}

Innerhalb der while-Schleife rufen wir &read_addr() auf, um einen Datensatz einzulesen, und wenn ein Datensatz gefunden wurde, durchsuchen wir ihn mit Hilfe der Subroutine &perform_search(). Sind wir am Ende der Adreßdatei angekommen und die Variable $bigmatch ist gleich 0, heißt das, dass keine Übereinstimmungen gefunden wurden, und wir informieren den Anwender darüber. Am Ende der Adreßdatei rufen wir jedoch auf alle Fälle last auf, um aus der Schleife auszusteigen und das Skript zu beenden.

Adressen einlesen

Mit der Subroutine &read_addr() wird ein Adreßdatensatz eingelesen. In Listing 14.1 sehen Sie den Inhalt von &read_addr().

Listing 14.1: Die Subroutine &read_addr()

1:  sub read_addr {
2: my %curr = (); # aktueller Datensatz
3: my $key = ''; # temp. Schlüssel
4: my $val = ''; # temp. Wert
5:
6: while (<>) {
7: chomp;
8: if ($_ ne '---') { # Datensatz-Trennzeichen
9: ($key, $val) = split(/: /,$_,2);
10: $curr{$key} = $val;
11: }
12: else { last; }
13: }
14: return %curr;
15: }

In früheren Beispielen mit while-Schleife, die <> verwendeten, haben wir die gesamte Datei auf einmal eingelesen und verarbeitet. Bei dieser while-Schleife ist das etwas anders; sie liest nur Teile der Datei ein und hört auf, wenn Sie auf ein Datensatz- Trennzeichen stößt (in diesem Fall der String '---'). Das nächste Mal, wenn die Subroutine &read_addr() aufgerufen wird, fährt die while-Schleife dort fort, wo sie in der Adreßdatei gestoppt hat. Perl hat keine Schwierigkeiten mit diesem Stop-and-Go der Eingabe und ist damit besonders geeignet zum Einlesen und Verarbeiten von Teilabschnitten einer Datei, wie sie hier vorliegen.

Konkret ausgedrückt, liest diese Subroutine eine Zeile ein. Lautet die Zeile nicht '---', so befindet sich die Zeile mitten in einem Datensatz und besteht aus dem Feldnamen (Name:, Telefon:, usw.) und dem Wert. Der Aufruf der split-Funktion erfolgt in Zeile 9. Beachten Sie das zweite Argument am Ende von split; damit wird angezeigt, dass es in jeder Datensatzzeile nur zwei Teile gibt. Mit dem Feldnamen ($key) und dem Wert ($val) können Sie beginnen, den Hash für diese Adresse einzurichten.

Handelt es sich bei der eingelesenen Zeile um die Datensatzende-Marke, springt die if-Anweisung in Zeile 8 direkt zum else-Teil in Zeile 12, wo der last-Befehl die Schleife verläßt. Das Ergebnis dieser Subroutine ist ein Hash, der alle Zeilen der Adresse nach Feldnamen indiziert enthält.

Die Suche durchführen

Inzwischen sind Sie in der Ausführung des Skripts soweit fortgeschritten, dass Sie einen Suchbegriff in der Variablen $search und eine Adresse in der Variablen $rec gespeichert haben. Der nächste Schritt besteht jetzt darin, zum nächsten Teil unser großen while-Schleife zu Beginn des Skripts überzugehen. Das heißt, wenn %rec definiert ist (also eine Adresse existiert), rufen wir die Subroutine &perform_search() auf, um konkret festzustellen, ob der Suchbegriff in $search auch mit der Adresse in &rec übereinstimmt.

Die Subroutine &perform_search() wird in Listing 14.2 gegeben:

Listing 14.2: Die Subroutine &perform_search()

1:  sub perform_search {
2: my ($str, %rec) = @_;
3: my $matched = 0; # Übereinstimmung
4: my $i = 0; # Position innerhalb des Suchmusters
5: my $thing = ''; # temporäres Wort
6:
7: my @things = $str =~ /("[^"]+"|\S+)/g; # in Suchelemente aufspalten
8:
9: while ($i <= $#things) {
10: $thing = $things[$i]; # Suchelement, UND oder ODER
11: if ($thing eq 'ODER' || $thing eq 'oder') { # OR Fall
12: if (!$matched) { # noch keine Übereinstimmung,
# nächstes Element
13: $matched = &isitthere($things[$i+1], %rec);
14: }
15: $i += 2; # ODER überspringen und nächstes Element
16: }
17: elsif ($thing eq 'UND' || $thing eq 'und') { # UND-Fall
18: if ($matched) {
# Übereinstimmung gefunden, andere Seite prüfen
19: $matched = &isitthere($things[$i+1], %rec);
20: }
21: $i += 2; # UND überspringen und nächstes Element
22: }
23: elsif (!$matched) { # noch keine Übereinstimmung
24: $matched = &isitthere($thing, %rec);
25: $i++; # weiter!
26: }
27: else { $i++; } # $match wurde gefunden, weiter zum
# nächsten Element
28: }
29:
30: if ($matched) { # alle Schlüssel durchgearbeitet, gab es
# eine Übereinstimmung?
31: $bigmatch = 1; # Ja, wir haben etwas gefunden
32: print_addr(%rec); # Datensatz ausgeben
33: }
34: }

Diese Subroutine ist sehr lang und etwas komplexer. Aber keine Bange, sie ist nicht so kompliziert, wie es den Anschein hat. Fangen wir oben an; dort übernimmt die Subroutine zwei Argumente: den Suchbegriff und den Adressen-Hash: Beachten Sie, dass diese Werte in globalen Variablen gespeichert sind. Deshalb gibt es an sich auch keinen Grund, sie als Argumente an die Subroutine zu übergeben. Wir hätten auf diese globalen Variablen einfach im Rumpf der Subroutine zugreifen können. Unsere Strategie, die Daten als Argumente zu übergeben, führt allerdings dazu, dass die Subroutine in sich abgeschlossener ist, da nur die Daten bearbeitet werden, die explizit übergeben werden. Sie könnten zum Beispiel diese Subroutine kopieren und in ein anderes Suchskript einfügen, ohne einen Gedanken an die Umbenennung von Variablen verschwenden zu müssen.

Die erste richtige Operation in dieser Subroutine erfolgt in Zeile 7, in der wir den Suchbegriff in seine Bestandteile aufsplitten. Denken Sie daran, dass der Suchbegriff in vielen Formen auftreten kann, einschließlich verschachtelter Strings in Anführungszeichen, UNDs und ODERs oder als eine Liste von Schlüsselwörtern. In Zeile 7 werden die einzelnen Elemente aus dem Suchbegriff extrahiert, und anschließend werden diese »Suchelemente« gemeinsam in dem Array @things gespeichert. Dabei sollten Sie beachten, dass der reguläre Ausdruck mit der Option g endet und in einem Listenkontext ausgewertet wird - das soll heißen, dass die @things-Liste alle möglichen von den Klammern eingefangenen Übereinstimmungen enthält. Womit stimmt dieses besondere Muster überein? Es gibt zwei Gruppen von Mustern, getrennt durch das Alternationszeichen (|). Die erste Gruppe lautet:

"[^"]+"

Dabei handelt es sich, wenn Sie an Ihre Muster zurückdenken, um ein doppeltes Anführungszeichen gefolgt von einem oder mehreren Zeichen, die kein doppeltes Anführungszeichen sind, und einem abschließenden Anführungszeichen. Dieses Muster vergleicht mehrteilige Strings im Suchbegriff (in Anführungszeichen), wie zum Beispiel »John Smith« oder »San Francisco«, und behandelt sie als einen Suchbegriff.

Der zweite Teil des Musters besteht lediglich aus einem oder mehreren Zeichen, die keine Whitespace-Zeichen sind (\S). Dieser Teil des Musters vergleicht alle einfachen Wörter, wie zum Beispiel UND oder ODER, oder einzelne Schlüsselwörter. Von diesen beiden Mustern wird ein langer, komplexer Suchbegriff wie »San Jose« ODER »San Francisco« UND John in folgende Liste aufgesplittet (»San Jose«, ODER, »San Francisco«, UND, John).

Nachdem nun alle unsere Suchteile in einer Liste stehen, besteht die eigentliche Arbeit darin, diese Liste durchzugehen, wenn nötig, nach der Adresse zu suchen und die logischen Ausdrücke abzuarbeiten. All dies wird in der großen while-Schleife ausgeführt, die in Zeile 9 beginnt. Die Schleife verwendet eine Platzhaltervariable $i zum Festhalten der aktuellen Position in dem Muster und geht das Muster bis zum Ende durch. Innerhalb der while-Schleife prüft die Variable $matched ständig, ob ein bestimmter Teil des Musters mit dem Datensatz übereinstimmt. Begonnen wird mit einer 0, falsch, für keine Übereinstimmung.

Innerhalb der while-Schleife starten wir in Zeile 10, wo wir der Variablen $things auf den aktuellen Teil des zu untersuchenden Musters zuweisen, einfach um uns bei weiteren Zugriffen Tipparbeit zu ersparen. Anschließend folgen vier größere Tests:

Wenn Sie mir soweit folgen konnten, haben Sie den schwierigsten Teil des Skripts bereits hinter sich. Sollten Sie noch Schwierigkeiten haben, versuchen Sie es einfach mal selbst mit verschiedenen Suchmustern: mit einzelnen Suchelementen, Elementen, die durch UND oder ODER getrennt sind, und Mustern mit mehreren Suchschlüsseln. Kontrollieren Sie die Werte von $i und $matched beim Schleifendurchlauf (wenn Sie mit dem Perl-Debugger bereits umgehen können, ist dies recht einfach, aber Sie können es auch auf Papier von Hand machen).

Was also passiert in der mysteriösen &isitthere()-Subroutine, die in der großen while-Schleife immer wieder aufgerufen wird? Nachdem Suchbegriff und Datensatz gegeben sind, findet hier die eigentliche Suche statt. Ich werde darauf verzichten, Ihnen hier den Inhalt von &isitthere() vorzustellen, Sie finden diese Subroutine in voller Länge als Teil des Gesamtcodes in Listing 14.3. Ich möchte Sie jedoch darauf hinweisen, dass die Subroutine lediglich den Inhalt des Adressen-Hash durchläuft und mit Hilfe eines regulären Ausdrucks den Suchbegriff mit jeder Zeile vergleicht. Bei einer Übereinstimmung liefert die Subroutine 1 zurück, bei keiner Übereinstimmung 0.

Im letzten Teil der Subroutine sind alle Teile des Suchbegriffs verarbeitet, einige Suchläufe wurden durchgeführt, und wir wissen, ob der Suchbegriff mit dem Datensatz übereinstimmt oder nicht. Die Zeilen 30 bis 33 testen, ob eine Übereinstimmung gefunden wurde. Wenn ja, setzen wir die Variable $bigmatch (mindestens eine Adresse wurde als Übereinstimmung gefunden) und rufen &print_addr() auf, um diese Adresse auszudrucken.

Den Datensatz ausdrucken

Von hier an wird es einfach. Die letzte Subroutine in der Datei wird nur aufgerufen, wenn eine Übereinstimmung gefunden wurde. Die Subroutine &print_addr() durchläuft einfach den Datensatz-Hash und gibt die Werte aus, um den Adreßdatensatz anzuzeigen.

sub print_addr {
my %record = @_;
print "*********************\n";
foreach my $key (qw(Name Telefon Fax Adresse EMail URL)) {
if (defined($record{$key})) {
print "$record{$key}\n";
}
}
}

Interessant an dieser Subroutine ist allein die Liste der Schlüssel in der foreach- Schleife. Ich habe die Schlüssel in dieser Reihenfolge aufgeführt (und mit der Funktion qw in Anführungszeichen gesetzt), so dass die Ausgabe in einer bestimmten Reihenfolge erfolgt. Da Hashes in einer intern festgelegten Reihenfolge gespeichert werden, können wir nur so dafür sorgen, dass die Daten in der korrekten Reihenfolge ausgegeben werden. Nebenbei wird dadurch auch erreicht, dass nur die Zeilen ausgegeben werden, die tatsächlich verfügbar waren - der Aufruf von defined innerhalb der foreach-Schleife stellt sicher, dass nur die Felder ausgegeben werden, die auch tatsächlich im Datensatz existieren.

Der Code

Alles OK? Nein? Manchmal ist es eine Hilfe, wenn man den ganzen Code auf einmal sieht. Listing 14.3 enthält den vollständigen Code für adressen.pl. Wenn Sie den Quelltext von der Website zu diesem Buch (unter http://www.typerl.com) heruntergeladen haben, werden Sie feststellen, dass der Code wesentlich mehr Kommentare enthält, um deutlich zu machen, was gerade passiert.

Wie ich bereits gestern in dem Abschnitt zu my-Variablen angedeutet habe, können einige Perl-Versionen Schwierigkeiten mit der Verwendung der my-Variablen im Skript und den foreach-Schleifen haben. Sie können das Problem umgehen, indem Sie die foreach-Variable einfach wie folgt vor ihrem Gebrauch deklarieren:

my $key = 0;

foreach $key (qw(Name Telefon Fax Adresse EMail URL)) { ...

Listing 14.3: Der Code für adressen.pl

1:  #!/usr/bin/perl -w
2: use strict;
3:
4: my $bigmatch = 0; # wurde etwas gefunden?
5: my %rec = (); # zu durchsuchender Datensatz
6: my $search = ''; # Suchmuster
7:
8: $search = &getpattern(); # Eingabeaufforderung für das Muster
9:
10: while () { # durchsucht die Adressdatei
11: %rec = &read_addr();
12: if (%rec) { # Datensatz gefunden
13: &perform_search($search, %rec);
14: } else { # Ende der Adressdatei, Aufräumarbeiten
15: if (!$bigmatch) {
16: print "Nichts gefunden.\n";
17: } else { print "*********************\n"; }
18: last; # Verlassen, wir sind fertig
19: }
20: }
21:
22: sub getpattern {
23: my $in = ''; # Eingabe
24: print 'Wonach soll gesucht werden? ';
25: chomp($in = <STDIN>);
26: return $in;
27: }
28:
29: sub read_addr {
30: my %curr = (); # aktueller Datensatz
31: my $key = ''; # temp. Schlüssel
32: my $val = ''; # temp. Wert
33:
34: while (<>) { # bricht bei EOF ab
35: chomp;
36: if ($_ ne '---') { # Datensatz-Trennzeichen
37: ($key, $val) = split(/: /,$_,2);
38: $curr{$key} = $val;
39: }
40: else { last; }
41: }
42: return %curr;
43: }
44:
45: sub perform_search {
46: my ($str, %rec) = @_;
47: my $matched = 0; # gesamte Übereinstimmung
48: my $i = 0; # Position innerhalb des Suchmusters
49: my $thing = ''; # temporäres Wort
50:
51: my @things = $str =~ /("[^"]+"|\S+)/g; # in Suchelemente aufspalten
52:
53: while ($i <= $#things) {
54: $thing = $things[$i]; # Suchelement, UND oder ODER
55: if ($thing eq 'ODER' || $thing eq 'oder') { # ODER-Fall
56: if (!$matched) { # noch keine Übereinstimmung, nächstes
# Element
57: $matched = &isitthere($things[$i+1], %rec);
58: }
59: $i += 2; # ODER überspringen und nächstes Element
60: }
61: elsif ($thing eq 'UND' || $thing eq 'und') { # UND-Fall
62: if ($matched) { # Übereinstimmung gefunden, andere Seite
# prüfen
63: $matched = &isitthere($things[$i+1], %rec);
64: }
65: $i += 2; # UND überspringen und nächstes Element
66: }
67: elsif (!$matched) { # noch keine Übereinstimmung
68: $matched = &isitthere($thing, %rec);
69: $i++; # weiter!
70: }
71: else { $i++; } # $match wurde gefunden, weiter zum
# nächsten Element
72: }
73:
74: if ($matched) { # alle Schlüssel durchgearbeitet, gab es
# eine Übereinstimmung?
75: $bigmatch = 1; # Ja, wir haben etwas gefunden
76: print_addr(%rec); # Datensatz ausgeben
77: }
78: }
79:
80: sub isitthere { # einfacher Test
81: my ($pat, %rec) = @_;
82: foreach my $line (values %rec) {
83: if ($line =~ /$pat/) {
84: return 1;
85: }
86: }
87: return 0;
88: }
89:
90: sub print_addr {
91: my %record = @_;
92: print "*********************\n";
93: foreach my $key (qw(Name Telefon Fax Adresse EMail URL)) {
94: if (defined($record{$key})) {
95: print "$record{$key}\n";
96: }
97: }
98: }

Ein Prozessor für Log-Dateien von Websites (weblog.pl)

Das zweite Beispielskript übernimmt eine Protokolldatei, wie Sie von Webservern erzeugt wird, und erstellt aus den darin enthaltenen Daten eine Statistik. Die meisten Webserver legen Dateien dieser Art an, mit denen sie unter anderem verfolgen, wie viele Besucher (»Hits«) es für eine Website gab, welche Dateien angefordert wurden und von welchen Sites die Anfragen kamen.

Im Web gibt es bereits viele Programme zur Analyse von Protokolldateien (einschließlich der Programme, die üblicherweise für Ihren Webserver zur Verfügung stehen). Deshalb ist dieses Beispiel auch nicht gerade neu. Die Statistiken, die damit erstellt werden, sind relativ einfach. Sie können dieses Skript jedoch ohne weiteres umschreiben, so dass es jede beliebige von Ihnen gewünschte Information mit aufnimmt. Es ist eine gute Ausgangsbasis zum Verarbeiten von Webprotokollen beziehungsweise ein gutes Muster, wenn Protokolldateien anderer Programme zu verarbeiten sind.

Funktionsweise

Das Skript weblog.pl wird mit einem Argument, der Protokolldatei, aufgerufen. Auf vielen Webservern werden diese Dateien access_log genannt und folgen dem sogenannten common log-Format. Das Skript arbeitet erst einmal eine Weile vor sich hin. Dabei gibt es die Datumsstempel der bearbeiteten Protokolle aus, damit Sie erkennen, dass das Skript noch arbeitet. Anschließend werden einige Ergebnisse ausgegeben. Sehen Sie im folgenden, wie eine Ausgabe aussehen kann (das untenstehende Beispiel ist von den Protokollen meines eigenen Webservers www.lne.com):

% weblog.pl access_log
Log-Dateien verarbeiten....
Verarbeite 09/Apr/1998
Verarbeite 10/Apr/1998
Verarbeite 11/Apr/1998
Verarbeite 12/Apr/1998
Auswertung der Log-Datei:
Gesamtzahl der Treffer: 55789
Gesamtzahl der fehlgeschlagenen Treffer: 1803 (3.23%)
(erfolgreiche) HTML-Dateien: 18264 (33.83%)
Anzahl der Hosts: 5911
Anzahl der Domänen: 2121
Die beliebtesten Dateien:
/Web/index.html (2456 Treffer)
/lemay/index.html (1711 Treffer)
/Web/Title.gif (1685 Treffer)
/Web/HTML3.2/3.2thm.gif (1669 Treffer)
/Web/JavaProf/javaprof_thm.gif (1662 Treffer)
Die beliebtesten Hosts:
202.185.174.4 (487 Treffer)
vader.integrinautics.com (440 Treffer)
linea15.secsa.podernet.com.mx (437 Treffer)
lobby.itmin.com (284 Treffer)
pyx.net (256 Treffer)
Die beliebtesten Domänen:
mindspring.com (3160 Treffer)
aol.com (1808 Treffer)
uu.net (792 Treffer)
grid.net (684 Treffer)
compuserve.com (565 Treffer)

Die Ausgabe hier zeigt, um Platz zu sparen, nur die ersten fünf Dateien, Hosts und Domänen. Sie können das Skript allerdings so konfigurieren, dass es eine beliebige Anzahl dieser Statistiken ausgibt.

Der Unterschied zwischen einem Host und einer Domäne mag vielleicht nicht direkt ersichtlich sein. Ein Host ist der vollständige Hostname des Systems, das auf den Webserver zugegriffen hat, und kann dynamisch zugewiesene Adressen und Proxy- Server mit umfassen. So unterscheidet sich der Host dialup124.servers.foo.com vom Host dialup567.servers.foo.com. Die Domäne andererseits bezeichnet eine größere Gruppe von mehreren Hosts und besteht in der Regel aus zwei oder drei Teilen. So ist foo.com ein Domäne, ebenso wie aol.com oder demon.co.uk. Die Domänen-Listings neigen dazu, einzelne Host-Einträge in größere Gruppen zusammenzufassen - alle Hosts im Wirkungsbereich von aol.com erscheinen in der Domänenliste als Hit von aol.com.

Beachten Sie, dass es sich bei einem einfachen Hit um eine HTML-Seite, ein Bild, ein eingereichtes Formular oder eine andere beliebige Datei handeln kann. Es gibt normalerweise wesentlich mehr Hits als tatsächliche Zugriffe auf eine Seite (page views). Dieses Skript macht den Unterschied deutlich, indem HTML-Anfragen getrennt von der Gesamtzahl der Hits gezählt werden.

Wie sieht ein Webprotokoll aus

Da das Skript weblog.pl Webprotokolle verarbeitet, ist es von Vorteil, wenn man weiß, wie diese Protokolldateien aussehen. Webprotokolle speichern einen Treffer pro Zeile und jede Zeile in dem sogenannten allgemeinen Protokollformat (allgemein, da das Format mehreren Webservern gemeinsam ist). Die meisten Webserver erzeugen ihre Protokolldateien in diesem Format beziehungsweise können entsprechend konfiguriert werden (viele Server verwenden eine erweiterte Form des allgemeinen Protokollformats, das mehr Informationen enthält). Eine Zeile einer Protokolldatei im allgemeinen Protokollformat kann folgendermaßen aussehen (diese Zeile steht aus drucktechnischen Gründen in zwei Zeilen, in Wirklichkeit steht sie jedoch nur in einer Zeile):

proxy2bh.powerup.com.au - - [03/Apr/1998:00:09:02 -0800]
"GET /lemay/ HTTP/1.0" 200 4621

Die einzelnen Elemente jeder Zeile der Protokolldatei sind:

Natürlich sind nicht all diese Elemente einer Protokolldatei von Interesse für ein Skript zum Aufbau einer Statistik, und viele werden Sie erst verstehen, wenn Sie wissen, wie Webserver funktionieren. Ein paar jedoch, wie der Host, das Datum, der Dateiname und der Rückgabecode können extrahiert und für jede Zeile der Datei verarbeitet werden.

Das Skript erstellen

Der logische Ablauf dieses Skripts ist leichter zu verfolgen als für das Skript adressen.pl. Es gibt eigentlich nur zwei größere Schritte - die Verarbeitung des Protokolls und die Erzeugung der Statistik. Dabei werden wir von einer Reihe von Subroutinen unterstützt.

Um genau zu sein, der gesamte Code für dieses Skript ist in Subroutinen untergebracht. Der Rumpf des Codes besteht aus einer Reihe von globalen Variablen und dem Aufruf von zwei Subroutinen: &process_log() und &print_results().

Die globalen Variablen dienen dazu, die verschiedenen statistischen Daten und sonstigen Informationen über Teile der Protokolldatei zu speichern. Da viele dieser statistischen Daten Hashes sind, wäre die Verwendung von lokalen Variablen und das Weiterreichen der Daten zu kompliziert. In diesem Fall ist die Verwaltung der Variablen einfacher, wenn sie globaler Art sind. Zu den globalen Daten, die wir verfolgen, gehören:

Darüber hinaus gibt es noch zwei weitere globale Variablen:

Diese zwei Variablen legen fest, wie sich das Skript selbst verhält. Wir hätten diese Variablen auch tief im Innern des Programms verbergen können. Doch dadurch, dass wir sie hier direkt am Anfang aufgeführt haben, können Sie oder alle anderen, die das Skript nutzen, das Gesamtverhalten des Skripts ändern, ohne nach den zu ändernden Variablen lange suchen zu müssen. Diese Verfahrensweise gehört zum »guten Programmierstil«, unabhängig davon, welche Programmiersprache Sie verwenden.

Das Protokoll verarbeiten

Der erste Teil des weblog.pl-Skripts besteht aus der Subroutine &process_log(), die die einzelnen Zeilen der Protokolldatei durchläuft und die statistischen Daten aus der Zeile speichert. Ich werde Ihnen nicht jede Zeile dieser Subroutine erläutern, aber ich zeige Ihnen die wichtigsten Teile. Den vollständigen Code können Sie in Listing 14.7 am Ende dieses Kapitels einsehen.

Das Kernstück der Subroutine &process_log() ist eine weitere while (<>)-Schleife, um die Zeilen der Eingabe einzeln einzulesen. Im Gegensatz zu adressen.pl liest dieses Skript die Datei von Anfang bis Ende ein.

Für die Verarbeitung der Zeile splitten wir sie zuerst in ihre Bestandteile und speichern diese Teile in einem Hash, dessen Schlüssel der Teilename ist ('site', 'file' und so weiter). Für das Aufsplitten gibt es eine separate Subroutine namens &splitline(), die in Listing 14.4 zu sehen ist.

Listing 14.4: Die Subroutine &splitline()

1:  sub splitline {
2: my $in = $_[0];
3: my %line = ();
4: if ($in =~ /^([^\s]+)\s # Site
5: ([\w-]+\s[\w-]+)\s # Benutzer
6: \[([^\]]+)\]\s # Datum
7: \"(\w+)\s # Protokoll
8: (\/[^\s]*)\s # Datei
9: ([^"]+)\"\s # HTTP-Version
10: (\d{3})\s # Rückgabe-Code
11: ([\d-]+) # übertragene Bytes
12: /x) {
13: $line{'site'} = $1;
14: $line{'date'} = $3;
15: $line{'file'} = $5;
16: $line{'code'} = $7;
17: return %line;
18: } else { return (); }
19: }

Das erste, was Ihnen bei dieser Subroutine wahrscheinlich ins Auge fällt, ist der nicht enden wollende reguläre Ausdruck in der Mitte von Zeile 4 bis Zeile 11. Er ist so häßlich, dass er sechs Zeilen belegt! Und Kommentare erforderlich macht! Dieser reguläre Ausdruck hat die Form erweiterter regulärer Ausdrücke. Ich habe diese Ausdrücke bereits in dem Abschnitt »Vertiefung« in Kapitel 5, »Mit Hashes arbeiten«, eingehend beschrieben. Hier eine kurze Zusammenfassung: Angenommen Sie haben einen besonders ekligen regulären Ausdruck wie den in diesem Beispiel (aus drucktechnischen Gründen steht er auf zwei Zeilen, da er nicht in eine Zeile paßt!):

if ($in =~ /^([^\s]+)\s([\w-]+\s[\w-]+)\s\[([^\]]+)\]\s\"(\w+)
\(\/[^\s]*)\s([^"]+)\"\s(\d{3})\s([\d-]+)/)

Sehr wahrscheinlich brauchen Sie entweder eine unendliche Geduld oder sehr starke Beruhigungsmittel, um diesen Ausdruck aufzuschlüsseln und zu verstehen. Und das Debuggen dieses Ausdrucks ist auch nicht sehr lustig. Wenn Sie jedoch an das Ende des Ausdrucks die Option /x setzen (wie hier in Zeile 12), können Sie den regulären Ausdruck aufsplitten und auf verschiedene Zeilen verteilen, die Sie außerdem noch mit Kommentaren versehen können. Alle Leerzeichen darin werden ignoriert. Wenn Sie im Text eine Übereinstimmung auf Leerzeichen suchen wollen, müssen Sie \s verwenden. Die Option /x erleichtert lediglich das Lesen und Debuggen des regulären Ausdrucks.

Unser regulärer Ausdruck geht von dem allgemeinen Protokollformat (common log format) aus, das ich bereits oben beschrieben habe:

Jedes Element dieses regulären Ausdrucks wird in einem geklammerten Ausdruck (und einer Übereinstimmungsvariablen) gespeichert, wobei die zusätzlichen Klammern oder Anführungszeichen entfernt werden. Sobald das Pattern Matching beendet ist, können wir die verschiedenen übereinstimmenden Teile in einem Hash ablegen. Beachten Sie, dass wir nur die Hälfte der Übereinstimmungen in dem Hash ablegen. Wir müssen nur das abspeichern, was wir am Ende auch nutzen wollen. Wenn Sie aber dieses Beispiel erweitern wollen, um Statistiken über weitere Teile der Treffer zu erstellen, müssen Sie lediglich Zeilen einfügen, die diese Übereinstimmungen dem Hash hinzufügen. Sie brauchen den regulären Ausdruck nicht zu verändern, um mehr Informationen zu erhalten.

Nachdem die Zeile jetzt in ihre einzelnen Elemente zerlegt ist, kehren wir von der Subroutine &splitline() zurück zu der Hauptroutine &process_log(). Diese Routine überprüft als nächstes alle fehlgeschlagenen Treffer. Wenn eine Zeile im Webprotokoll nicht dem Muster entspricht - was bei einigen der Fall ist -, liefert die Subroutine &splitline() Null zurück. Dieses Ergebnis wird als fehlgeschlagener Treffer interpretiert, der dann zu der Zahl der fehlgeschlagenen Treffer addiert wird. Anschließend wird der Rest der Schleife übersprungen, um mit der nächsten Zeile fortzufahren:

if (!%hit) {  # mißgestaltete Zeile im Webprotokoll
$failhits++;
next;
}

Der nächste Schritt im Skript ist ein Entgegenkommen an all diejenigen, die das Skript ausführen. Die Verarbeitung einer beliebig großen Protokolldatei kann lange dauern, und manchmal ist es schwer zu sagen, ob Perl noch die Protokolldatei bearbeitet oder ob sich das System aufgehängt hat und keinen Wert mehr liefern wird. Dieser Teil des Skripts gibt eine Nachricht mit dem Datum der Zeilen aus, die gerade verarbeitet werden. Jedesmal, wenn die Treffer eines Tages vollständig bearbeitet worden sind, erscheint eine neue Nachricht, die das Fortschreiten von Perl in der Datei anzeigt:

$dateshort = &getday($hit{'date'});
if ($currdate ne $dateshort) {
print "Verarbeite $dateshort\n";
$currdate = $dateshort;
}

In diesem Fragment ist &getday() eine kurze Subroutine, die den Monat und den Tag aus dem Datumsfeld ausliest. Dabei wird ein Muster verwendet, so dass Monat und Tag mit dem gerade verarbeiteten Datum verglichen werden können (auf den Ausdruck des Codes für &getday() verzichte ich, da Sie ihn in dem vollständigen Listing am Ende des Kapitels finden). Sind sie unterschiedlich, wird eine Nachricht ausgegeben und die Variable $currdate aktualisiert.

Zusätzlich zu den Zeilen in der Protokolldatei, die nicht dem Protokollformat entsprechen, werden auch jene Zeilen als fehlgeschlagene Treffer bezeichnet, die zwar dem Muster entsprechen, aber nicht dazu führten, dass wirklich eine Datei zurückgeliefert wurde (falsche URL-Angaben oder Dateien, die verschoben wurden, lösen diese Art von Treffer aus). Diese Treffer werden in der Protokolldatei mit einem Fehlercode aufgezeichnet, der mit 4 beginnt (vielleicht ist Ihnen bereits der Error 404 im Web aufgefallen). Der Rückgabecode gehört mit zu den Elementen der Zeile, die wir gespeichert hatten. Deshalb ist die Überprüfung ein einfacher Musterabgleich:

if ($hit{'code'} =~ /^4/) { # 404, 403, etc. (Fehler)
$failhits++;

Der else-Teil dieser if-Anweisung betrifft alle anderen Treffer - gemeint sind damit alle erfolgreichen Treffer, die tatsächlich HTML-Dateien oder Grafiken zurückgeliefert haben. Diese Treffer haben einen Rückgabecode von 200 oder 304.

} elsif ($hit{'code'} =~ /200|304/) {   # behandelt nur erfolgreiche Treffer

Webserver sind so eingerichtet, dass sie eine Standarddatei, in der Regel index.html, zurückliefern, wenn eine URL angefordert wird, die einem Verzeichnisnamen entspricht. Das bedeutet, dass eine Anfrage nach /web/ und eine Anfrage nach /web/ index.html sich auf die gleiche Datei beziehen, jedoch in der Protokolldatei als unterschiedliche Einträge erscheinen. Um Verzeichnisse und Standarddateien als einen Eintrag zu behandeln, gibt es einige Zeilen, die prüfen, ob die angeforderte Datei mit einem Slash endet, und wenn ja, dafür sorgen, dass der Standarddateiname hinten angehängt wird. Die Standarddatei, wie ich bereits oben erwähnt habe, wird durch die Variable $default definiert:

if ($hit{'file'} =~ /\/$/) { # slashes werden zu $default
$hit{'file'} .= $default;
}

Nachdem wir dies erledigt haben, können wir die Verarbeitung damit abschließen, dass wir die Variable $htmlhits inkrementieren, wenn es sich bei der Datei um eine HTML-Datei handelt, und die Hashes für die Site und für die Datei aktualisieren:

if ($hit{'file'} =~ /\.html?$/) { # .htm oder .html
$htmlhits++;
}

$hosts{ $hit{'site'} }++;
$files{ $hit{'file'} }++;

Damit sind wir ans Ende der while-Schleife angelangt, die mit der nächsten Zeile in der Datei von vorne beginnt. Die Schleife wird so lange durchlaufen, bis alle Zeilen verarbeitet sind. Danach gehen wir zum Ausgeben der Ergebnisse dieses Skripts über.

Die Ergebnisse ausgeben

Die Subroutine &process_log() verarbeitet die Protokolldatei zeilenweise und ruft zu ihrer Unterstützung die Subroutinen &splitline() und &getday() auf. Der zweite Teil unseres weblog.pl-Skripts besteht aus der Subroutine &print_results(), die ebenfalls auf einige weitere Subroutinen zur Unterstützung zurückgreift. Der größte Teil der Subroutine besteht jedoch aus einer Reihe von print-Anweisungen, um die verschiedenen Statistiken auszugeben.

Die ersten Zeilen geben die Gesamtzahl der Treffer, Gesamtzahl der fehlgeschlagenen Treffer und Gesamtzahl der HTML-Treffer aus. Die letzteren werden auch als Prozent der gesamten Treffer aufgeschlüsselt, wobei die HTML-Treffer sich nur auf die Gesamtsumme der erfolgreichen Treffer beziehen. Wir erhalten diese Werte mit ein wenig Mathematik und der Anweisung printf:

print "Auswertung der Log-Datei:\n";
print "Gesamtzahl der Treffer: $totalhits\n";
print "Gesamtzahl der fehlgeschlagenen Treffer: $failhits (";
printf('%.2f', $failhits / $totalhits * 100);
print "%)\n";
print "(erfolgreiche) HTML-Dateien: $htmlhits (";
printf('%.2f', $htmlhits / ($totalhits - $failhits) * 100);
print "%)\n";

Als nächstes kommt die Gesamtzahl der Hosts. Diesen Wert erhalten wir, indem wir die Schlüssel aus dem Hash %hosts herausziehen und in einer Liste ablegen. Anschließend werten wir diese Liste in einem skalaren Kontext aus (mit Hilfe der Funktion scalar).

print 'Anzahl der Hosts: ';
print scalar(keys %hosts);
print "\n";

Um die Anzahl der Domänen zu ermitteln, müssen wir den Hash %hosts verarbeiten, um die Hosts ihren Domänen zuzuordnen, und einen neuen Hash (%domains) einrichten, der die Anzahl der Treffer für die Domänen aufnimmt. Dazu verwenden wir eine Subroutine namens &getdomains(), die ich im nächsten Abschnitt besprechen werde. Gehen wir einfach davon aus, dass wir unseren Hash %domains bereits haben. Wir können auf die Schlüssel dieses Hash den gleichen Trick mit scalar anwenden, um die Anzahl der Domänen zu ermitteln:

my %domains = &getdomains(keys %hosts);
print 'Anzahl der Domänen: ';
print scalar(keys %domains);
print "\n";

Als letztes sind die beliebtesten Dateien, Hosts und Domänen auszudrucken. Für die Ermittlung dieser Werte gibt es die Subroutine &gettop(), die jeden Hash nach seinen Werten sortiert (die Häufigkeit, mit der jede Datei, Host oder Domäne in einem Treffer vorkam) und dann einen Array deskriptiver Strings einrichtet mit den Schlüsseln und Werten im Hash. Das Array enthält nur die 5 oder 10 (oder was auch immer Sie als Wert in $topthings ablegen) beliebtesten Dateien, Hosts oder Domänen. Doch gleich mehr zu der Subroutine &gettops().

Jedes dieser Arrays wird zum Schluß ausgegeben. Hier sehen Sie den Code für die Ausgabe der Dateien:

print "Die beliebtesten Dateien: \n";
foreach my $file (&gettop(%files)) {
print " $file\n";
}

Die Subroutine &getdomains()

Noch sind wir nicht fertig. Es fehlen uns noch die Hilfsroutinen zur Ausgabe der Statistiken: &getdomains(), um die Domänen aus dem %hosts-Hash zu extrahieren und die Statistik neu zu berechnen, und &gettop(), um einen Hash von Schlüsseln und Frequenzwerten zu übernehmen und die beliebtesten Elemente zurückzuliefern. Die Subroutine &getdomains() finden Sie in Listing 14.5.

Listing 14.5: Die Subroutine &getdomains()

1:  sub getdomains {
2: my %domains = ();
3: my ($sd,$d,$tld); # sekundäre Domäne, Domäne, oberste Domäne
4: foreach my $host (@_) {
5: my $dom = '';
6: if($host =~ /(([^.]+)\.)?([^.]+)\.([^.]+)$/ ) {
7: if (!defined($1)) { # nur zwei Domänen (i.e. aol.com)
8: ($d,$tld) = ($3, $4);
9: } else { # eine gewöhnliche Domäne x.y.com etc
10: ($sd, $d, $tld) = ($2, $3, $4);
11: }
12: if ($tld =~ /\D+/) { # ignoriert reine IP-Zahlen
13: if ($tld =~ /com|edu|net|gov|mil|org$/i) { # US TLDs
14: $dom = "$d.$tld";
15: } else { $dom = "$sd.$d.$tld"; }
16: $domains{$dom} += $hosts{$host};
17: }
18: } else { print "Fehlerhaft: $host\n"; }
19: }
20: return %domains;
21: }

Diese Subroutine ist nicht so kompliziert, wie sie aussieht. Ich gehe dabei von ein paar Grundvoraussetzungen für den Hostnamen aus: dass zum Beispiel jeder Hostname aus mehreren Teilen besteht, die durch Punkte getrennt sind, und dass die Domäne abhängig von ihrem Namen entweder aus den zwei oder drei ganz rechts stehenden Teilen besteht. In dieser Subroutine werden wir dann jeden Host auf seine eigentliche Domäne reduzieren und dann diesen Domänennamen als Index für einen neuen Hash nutzen, wobei wir alle ursprünglichen Treffer für den Hostnamen in dem neuen domänenbasierten Hash speichern.

Die Hauptarbeit dieser Subroutine wird in der foreach-Schleife, die in Zeile 4 startet, geleistet. Das Argument, das dieser Subroutine übergeben wird, ist ein Array mit den Hostnamen aus dem %hosts-Array. Dabei durchläuft die Schleife alle Hostnamen, um sicherzustellen, dass sie alle berücksichtigt werden.

Der erste Teil der foreach-Schleife ist der lange und abschreckende reguläre Ausdruck in Zeile 6. Dieser Ausdruck greift sich die letzten zwei Teile des Hostnamens und, wenn es kann, auch die letzten drei (einige Hostnamen bestehen nur aus zwei Teilen, die jedoch auch vom regulären Ausdruck erfaßt werden). Von Zeile 7 bis 11 wird geprüft, wie viele Teile wir haben (2 oder 3). Diese Teile werden den Variablen $sd, $d und $tld zugewiesen ($sd steht für sekundäre Domäne, $d für Domäne und $tld für Top-Level-Domäne, falls Sie sie auseinanderhalten wollen).

Der zweite Teil der Schleife legt fest, ob wir zwei oder drei Teile des Hostnamens als eigentliche Domäne verwenden wollen, und ignoriert in Zeile 12 alle Hosts, die aus IP-Nummern anstelle von eigentlichen Domänennamen bestehen. Die rein willkürliche Regel, nach der ich entschieden habe, ob eine Domäne aus zwei oder drei Teilen besteht, lautet: Handelt es sich bei der Top-Level-Domäne (den am weitesten rechts gelegenen Teil des Hostnamens) um eine US-Domäne wie .com, .edu etc. (die vollständige Liste sehen Sie in Zeile 13), dann hat die Domäne nur zwei Teile. Dazu gehören aol.com, mit.edu, whitehouse.gov etc. Lautet die Top-Level-Domäne anders, ist es mit großer Wahrscheinlichkeit eine landesspezifische Domäne wie .us, .au, .mx etc. Diese Domänen verwenden in der Regel drei Teile, um auf eine Site Bezug zu nehmen (zum Beispiel citygate.co.uk oder monash.edu.au). Zwei Teile wären in diesem Falle nicht genau genug (edu.au bezieht sich auf alle Universitäten in Australien und nicht auf eine spezielle namens edu).

Das ist also die Aufgabe der Zeilen 13 bis 15: einen Domänennamen aus zwei oder drei Teilen zusammenzusetzen und in dem String $dom zu speichern. Wenn wir den Domänennamen zusammengesetzt haben, können wir ihn als Schlüssel für den neuen Hash verwenden und die Treffer, die wir für den ursprünglichen Host ermittelt haben, übertragen (Zeile 16). Nachdem der Domänen-Hash eingerichtet ist, sollten alle Treffer in dem Host-Hash auch in dem Domänen-Hash berücksichtigt sein, so dass wir diesen Hash an die Subroutine &print_results zurückgeben können.

Noch eine Sache: In Zeile 18 prüft die Subroutine auf Fehler im Hostnamen. Wenn der Ausdruck des Mustervergleichs in Zeile 6 zu keiner Übereinstimmung führt, muss in der Tat ein sehr seltsamer Hostname vorliegen, und wir geben eine entsprechende Nachricht aus. Im allgemeinen sollte eine solche Nachricht allerdings nicht erscheinen, da ein abartiger Hostname in der Protokolldatei normalerweise bedeutet, dass ein abartiger Hostname auf dem Host selbst vorliegt, was eigentlich über das Internet nur schwer möglich sein dürfte.

Die Subroutine &gettop()

Noch eine Subroutine, und dann können wir den Lehrstoff dieser Woche zur Seite legen, ein Bierchen trinken und feiern, dass wir zwei Drittel dieses Buches bereits bewältigt haben. Die letzte Subroutine &gettop() übernimmt einen Hash, sortiert ihn nach Werten und schneidet dann die obersten X Elemente ab, wobei X durch die Variable &topthings gegeben ist. Die Subroutine liefert ein Array von Strings zurück, wobei jeder String den Schlüssel und den Wert für die obersten X Elemente in einer Form enthält, die leicht durch die Subroutine &print_results(), von der aus diese Subroutine aufgerufen wurde, ausgegeben werden kann. Sehen Sie dazu das Listing 14.6.

Listing 14.6: Die Subroutine &gettop()

1:  sub gettop {
2: my %hash = @_;
3: my $i = 1;
4: my @topkeys = ();
5: foreach my $key (sort { $hash{$b} <=> $hash{$a} } keys %hash) {
6: if ($i <= $topthings) {
7: push @topkeys, "$key ($hash{$key} hits)";
8: $i++;
9: }
10: }
11: return @topkeys;
12: }

Der Code

Listing 14.7 enthält den vollständigen Code für das Skript weblog.pl.

Je nach Perl-Version sollten Sie auch hier an die my-Variablen innerhalb der foreach-Schleifen denken. Details finden Sie in dem Hinweis direkt vor dem Listing 14.3.

Listing 14.7: Der Code für weblog.pl

1:  #!/usr/bin/perl -w
2: use strict;
3:
4: my $default = 'index.html'; # Angabe Ihrer Standard-HTML-Datei
5: my $topthings = 30; # Anzahl der zu protokollierenden
# Dateien, Sites etc.
6: my $totalhits = 0;
7: my $failhits = 0;
8: my $htmlhits = 0;
9: my %hosts= ();
10: my %files = ();
11:
12: &process_log();
13: &print_results();
14:
15: sub process_log {
16: my %hit = ();
17: my $currdate = '';
18: my $dateshort = '';
19: print "Log-Dateien verarbeiten....\n";
20: while (<>) {
21: chomp;
22: %hit = splitline($_);
23: $totalhits++;
24:
25: # Prüfen auf fehlerhafte Zeilen
26: if (!%hit) { # fehlerhafte Zeilen im Webprotokoll
27: $failhits++;
28: next;
29: }
30:
31: $dateshort = &getday($hit{'date'});
32: if ($currdate ne $dateshort) {
33: print "Verarbeite $dateshort\n";
34: $currdate = $dateshort;
35: }
36:
37: # nach 404ern suchen
38: if ($hit{'code'} =~ /^4/) { # 404, 403, etc. (Fehler)
39: $failhits++;
40: # andere Dateien
41: } elsif ($hit{'code'} =~ /200|304/) {
# nur im Erfolgsfall bearbeiten
42: if ($hit{'file'} =~ /\/$/) { # slashes werden zu $default
43: $hit{'file'} .= $default;
44: }
45:
46: if ($hit{'file'} =~ /\.html?$/) { # .htm oder .html
47: $htmlhits++;
48: }
49:
50: $hosts{ $hit{'site'} }++;
51: $files{ $hit{'file'} }++;
52: }
53: }
54: }
55:
56: sub splitline {
57: my $in = $_[0];
58: my %line = ();
59: if ($in =~ /^([^\s]+)\s # Site
60: ([\w-]+\s[\w-]+)\s # Benutzer
61: \[([^\]]+)\]\s # Datum
62: \"(\w+)\s # Protokoll
63: (\/[^\s]*)\s # Datei
64: ([^"]+)\"\s # HTTP-Version
65: (\d{3})\s # Rückgabe-Code
66: ([\d-]+) # übertragene Bytes
67: /x) {
68: # wir sind nur an bestimmten Daten interessiert
69: # (zufällig jede 2. Information)
70: $line{'site'} = $1;
71: $line{'date'} = $3;
72: $line{'file'} = $5;
73: $line{'code'} = $7;
74: return %line;
75: } else { return (); }
76: }
77:
78: sub getday {
79: my $date;
80: if ($_[0] =~ /([^:]+):/) {
81: $date = $1;
82: return $date;
83: } else {
84: return $_[0];
85: }
86: }
87:
88: sub print_results {
89: print "Auswertung der Log-Datei:\n";
90: print "Gesamtzahl der Treffer: $totalhits\n";
91: print "Gesamtzahl der fehlgeschlagenen Treffer: $failhits (";
92: printf('%.2f', $failhits / $totalhits * 100);
93: print "%)\n";
94:
95: print "(erfolgreiche) HTML-Dateien: $htmlhits (";
96: printf('%.2f', $htmlhits / ($totalhits - $failhits) * 100);
97: print "%)\n";
98:
99: print 'Anzahl der Hosts: ';
101: print scalar(keys %hosts);
102: print "\n";
103:
104: my %domains = &getdomains(keys %hosts);
105: print 'Anzahl der Domänen: ';
106: print scalar(keys %domains);
107: print "\n";
108:
109: print "Die beliebtesten Dateien: \n";
110: foreach my $file (&gettop(%files)) {
111: print " $file\n";
112: }
113: print "Die beliebtesten Hosts: \n";
114: foreach my $host (&gettop(%hosts)) {
115: print " $host\n";
116: }
117:
118: print "Die beliebtesten Domänen: \n";
119: foreach my $dom (&gettop(%domains)) {
120: print " $dom\n";
121: }
122: }
123:
124: sub getdomains {
125: my %domains = ();
126: my ($sd,$d,$tld); # sekundäre Domäne, Domäne, oberste Domäne
127: foreach my $host (@_) {
128: my $dom = '';
129: if($host =~ /(([^.]+)\.)?([^.]+)\.([^.]+)$/ ) {
130: if (!defined($1)) { # nur zwei Domänen (i.e. aol.com)
131: ($d,$tld) = ($3, $4);
132: } else { # eine normale Domäne x.y.com etc
133: ($sd, $d, $tld) = ($2, $3, $4);
134: }
135: if ($tld =~ /\D+/) { # ignoriert reine IP-Zahlen
136: if ($tld =~ /com|edu|net|gov|mil|org$/i) { # US TLDs
137: $dom = "$d.$tld";
138: } else { $dom = "$sd.$d.$tld"; }
139: $domains{$dom} += $hosts{$host};
140: }
141: } else { print "Fehlerhaft: $host\n"; }
142: }
143: return %domains;
144: }
145:
146: sub gettop {
147: my %hash = @_;
148: my $i = 1;
149: my @topkeys = ();
150: foreach my $key (sort { $hash{$b} <=> $hash{$a} } keys %hash) {
151: if ($i <= $topthings) {
152: push @topkeys, "$key ($hash{$key} hits)";
153: $i++;
154: }
155: }
156: return @topkeys;
157: }

Zusammenfassung

Meistens wird Ihnen das Wissen in Programmierbüchern mit vielen Worten, aber zu wenigen praktischen Codebeispielen vermittelt, so dass Sie oft Schwierigkeiten haben, das Erlernte umzusetzen. Ich möchte zwar nicht behaupten, dass dieses Buch zu den wortkargen gehört, mit diesen Beispielkapiteln möchte ich Ihnen jedoch etwas längere Programme präsentieren, die richtige Probleme lösen und demonstrieren, wie ein reales Skript zusammengesetzt wird.

In der heutigen Lektion haben wir zwei längere Skripts unter die Lupe genommen: eine einfache Adreßdatei mit Suchfunktionen, die eine textbasierte Datenbank mit Namen und Adressen verwendet. Das Skript zur Verarbeitung dieser Datei ermöglicht es Ihnen, ein relativ komplexes Suchmuster zu verarbeiten, einschließlich dem Verschachteln logischer Ausdrücke, und das Zusammenstellen von Wörtern und Phrasen mit Hilfe von Anführungszeichen. Sie könnten dieses Beispiel so erweitern, dass Sie damit so ziemlich jede Situation meistern, in der eine komplexe Suche über Teile einer Datendatei ausgeführt werden soll: zum Beispiel um Mail-Nachrichten anhand von bestimmten Kriterien aus einem Mail-Ordner zu filtern oder nach besonderen Comic-Büchern aus einer Sammlung von Comics zu suchen. Jede Textdatei kann als einfache Datenbank dienen, und dieses Skript kann sie durchsuchen, solange es dahingehend modifiziert wurde, die Daten dieser Datenbank zu verarbeiten.

Das zweite Beispiel war ein Skript zur Auswertung von Log-Dateien, das Protokolle von Web-Servern verarbeitet und Statistiken ausgibt. Reine Protokolle sind häufig vom Äußeren ziemlich abschreckend. Dieses Skript liefert Ihnen einige grundlegende Informationen über das, was auf einer Website so alles abläuft. Dabei bediente es sich einiger komplexer regulärer Ausdrücke und einer Menge von Hashes, um die Rohdaten zu speichern. Sie könnten dieses Beispiel dahingehend erweitern, dass es auch andere Statistiken erstellt (zum Beispiel um Histogramme über die Anzahl der Treffer pro Tag oder pro Stunde anzulegen oder außer HTML-Dateien auch Bild- oder andere Dateien zu verfolgen). Oder Sie könnten Änderungen vornehmen, so dass andere Protokollarten (Mail-Protokolle, FTP-Protokolle oder was gerade anfällt) statistisch erschlossen werden.

Meinen Glückwunsch zum erfolgreichen Abschluß der zweiten Woche dieses dreiwöchigen Exkurses. Nach dieser Woche haben Sie bereits ein Großteil der Skriptsprache aufgenommen, so dass Sie jetzt in der Lage sein sollten, bereits einige Aufgaben in Perl zu lösen. Ab jetzt werden wir auf das Erlernte aufbauen. Also auf zu Woche 3!



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH